{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 实例7：用Python操作Word批量生成合同"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们在实例5中演示了如何用Python批量生成word版邀请函。我们是简单粗暴地找到需要填写受邀者信息所在位置（即run），然后将这个run直接替换成受邀者的公司名及姓名。因为只有一处需要替换，所以这个方法行得通，但遇到合同，一般有十来处需要修改，如果也逐个去找其位置所在的run，那就反而会降低我们的工作效率，背离办公自动化的初衷了。实例5可以作为入门`python-docx`模块的练手项目。\n",
    "\n",
    "对于合同的批量处理，我们将使用更聪明的办法。我们的思路是，先建立一个word模板，在合同里面需要变动信息的地方用“【....】”来代替，比如“【合同编号】”等。然后再建一个Excel文档，将“【合同编号】”等信息作为标题，将不同的合同信息放入这个Excel的每一行。然后用`python-docx`去读取word模板中的所有内容，凡是遇到“【....】”的字符，就用Excel中的对应标题下的信息去进行替换。Excel中从第二行开始每一行代表一个合同内需要填入的信息。\n",
    "\n",
    "我们建立的模板和合同信息如下图所示：\n",
    "![](images\\data_preparation.png)\n",
    "这里有几个注意事项：\n",
    "1. Excel文档中数字需要改成文本格式，不然像合同编号20190401在写入到word时会变成20190401.0。至于怎么转格式，请参考度娘：https://jingyan.baidu.com/article/ae97a646b3d0b7bbfc461d68.html\n",
    "2. Excel中的公式需要去除，不然填到word中的信息是公式，而不是值。\n",
    "3. Word模板中的“【....】”和Excel中的标题必须一一对应，且必须是全中文或全英文字符，因为`python-docx`会将中英混合的内容视为两个及以上的格式（run），导致在替换的时候无法正确识别。\n",
    "4. Word模板做好后，要用`python-docx`读取一下，看看“【....】”是不是一个独立的run，若不是，则需要从Excel标题栏中重新复制，覆盖word模板中的“【....】”信息，已保证这一串字符是一个run。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "合同编号：\n",
      "【合同编号】\n",
      " \n",
      "【货物名称】\n",
      "采购\n",
      "合同\n",
      "甲方\n",
      "：\n",
      "【采购方】\n",
      " \n",
      "乙方\n",
      "：\n",
      "ABC商贸\n",
      "有限责任公司\n",
      "签订时间：\n",
      "    \n",
      "年\n",
      "  \n",
      "月\n",
      "  \n",
      "日\n",
      "签订地点\n",
      "：\n",
      "甲方和乙方\n",
      "经过平等协商，在真实、充分地表达各自意愿的基础上，根据《中华人民共和国合同法》的规定，达成如下协议：\n",
      " \n",
      "1.货物的质量标准，按照\n",
      "行业标准\n",
      "执行。\n",
      "2.货物的包装物的供应：包装物随货出售，由乙方负责货物包装物供应。乙方应提供货物运至合同规定的交货地点所需要的包装，以防止货物在转运中损坏或变质。这类包装应采取防潮、防晒、防锈、防腐蚀、防震动及防止其它损坏的必要保护措施，从而保护货物能够经受多次搬运、装卸及长途运输。\n",
      "二、交货规定\n",
      "1.交货方法：由乙方送货(国家主管部门规定有送货办法的，按规定的办法执行；没有规定送货办法的，按双方协议执行)；\n",
      "2.运输方式：由乙方自行选择运输方式，运输及保\n",
      "险费用由\n",
      "【运费支付方】\n",
      "负担。货物交付给甲方之前，货物相关全部风险由乙方承担。\n",
      "3.交货地点：\n",
      "  \n",
      "【交货地点】\n",
      "  \n",
      "4.交货日期：乙方应在合同签订\n",
      "后\n",
      "3\n",
      "0\n",
      "天内完成交货，并附上双方约定的、记录货物相关事项的资料。。\n",
      "5\n",
      ".\n",
      " \n",
      "当乙方不能按时交付全部或部分的货物，或者存在这种可能性时，乙方应及时将原因及预定交货日期通知给甲方，并按照甲方的指示，迅速制定必要的对策。\n",
      "三、验收方法\n",
      "1.所有货物由乙方送到交货地点且甲方确认收货后\n",
      "【确认收货天数】\n",
      "天内，\n",
      "由甲乙双方共同对货物的包装、外观、数量、商标、型号、规格及性能等进行验收，签署检验报告。如乙方未按约定到甲方指定地点参加检验的，应视为乙方对甲方单方检验的结果予以确认。验收标准执行合同规定的货物质量标准。\n",
      "如发现乙方所交的货物有任何不符合合同规定之处，应做好记录，\n",
      "并由双方代表签字，作为甲方向乙方提出维修或退换货的依据。\n",
      "检验报告仅证明乙方所提供的货物截至出具检验报告之日时可以按合同要求予以接受，但不能视为乙方对货物存在的潜在缺陷所应付的责任的解除。此检验不作为对货物内在质量认定的依据。\n",
      "2.乙方所提供的货物应充分满足甲方使用的要求,确保供货货物的尺寸、规格、质量符合合同规定，甲方发出的询价函与乙方发出的报价书中规定的内容与合同具有同等的约束力。本合同内的货物质量保证期\n",
      "为\n",
      "【质保期】\n",
      "月，自\n",
      "验收通过之日起计算。质量保证期间如货物出现质量问题，甲方有权要求乙方维修或退换货，乙方应在接到甲方通知后\n",
      "5\n",
      "天内免费派人维修、退换符合质量要求的货物\n",
      "。\n",
      "四、合同价格与支付方式\n",
      "1.合同金额：本合同总价（已含税）为￥\n",
      " \n",
      "【合同总价】\n",
      " \n",
      "（大写：\n",
      " \n",
      "【总价大写】\n",
      " \n",
      "）。其中不含税价\n",
      " \n",
      "【不含税价】\n",
      " \n",
      "元，税率\n",
      "  \n",
      "【税率】\n",
      "%，\n",
      "税额\n",
      " \n",
      "【税额】\n",
      " 元。\n",
      "总价指甲方的交货价格，该价格应包含货物价格、通关费、包装费、物流费、装卸费、保险费、在甲方指定场所的安装调试费、必要的培训费以及增值税等一切费用。在乙方完全履行本合同下全部义务的情形下，除非另有书面约定，否则合同金额是甲方应当向乙方支付的全部费用 。\n",
      "2.支付方式：\n",
      "合同签订后，甲方在乙方提供正规增值税专用发票后\n",
      "5\n",
      "日内支付\n",
      "合同总价的\n",
      "【首付比例】\n",
      "%。\n",
      "3.质保金：本合同质保金为合同总价的\n",
      "2\n",
      "0\n",
      "%，合同质量保证期结束后，甲方支付\n",
      "合同尾款。前提是，乙方应完成甲方在质量保证期届满前提出的索赔和赔偿，维修和退换货等义务，否则甲方有权拒绝支付质保金。\n",
      "4\n",
      ".\n",
      " \n",
      "乙方的开户银行、帐户名称、银行帐号以本合同提供的为准，如有变更，乙方应在合同规定的相关付款期限十天之前以书面方式通\n",
      "知甲方并加盖财务专用章。如未正确依照上述规定执行而影响相关款项的支付，则由此产生的责任由乙方自负。\n",
      "五、权利侵害的防止\n",
      "1.乙方保证，交付的商品不存在任何违反相关的法律法规、政府指令或政策等或侵犯了第三者的合法权利（包括但不限于知识产权）的情况。\n",
      "2.如存在上述情况，乙方必须事前通知甲方，并应负责解决由此而产生的侵权纠纷。\n",
      "3.乙方违反本条第1款的保证，而与第三方产生纠纷时，应对由此而引起的纠纷承担所有的法律责任，并承担甲方因此造成的全部损失，甲方对该纠纷不承担任何责任，损失及费用。\n",
      "如果货物或货物的任何部分，因最终裁决构成侵权，其使用被限制，乙方应自担费用并主动做出相应的安排：或为甲方获取继续使用受指控侵权的货物或货物的某一部分的权利，或用不会造成侵权的同等技术水平的货物更换。同时，乙方应按照本合同总金额\n",
      "【损失赔偿比例】\n",
      "%赔偿甲方因此受到的损失，且不能免除乙方履行本合同的义务。\n",
      "六、不可抗力\n",
      "1.甲乙任何一方因自然灾害、战争、疾病或其他甲乙任何一方均不能预见、不能避免且不能克服的事由（以下简称“不可抗力”），延迟履行或不能完全履行本合同规定的责任和义务的，遭遇不可抗力事由的该当事人可免除承担相应的违约责任。\n",
      "2.由于不可抗力，甲乙任何一方未履行本合同项下的己方义务时，直至该当事人履行该义务止的期间，另一方可停止履行本合同项下的己方义务，而无需对该当事人承担任何责任。\n",
      "七、乙方责任\n",
      "1.乙方逾期交货的，每延期一天，乙方应向甲方以延期交货部分货款总\n",
      "值\n",
      "【罚金比例】\n",
      "%的罚金，并承担甲方因此所受的损失费用。\n",
      "2.乙方在合同约定的交货日期届满后\n",
      "3\n",
      "0\n",
      "天内仍\n",
      "不能交货的，甲方有权解除合同，且乙方应向甲方偿付不能交货部分货款\n",
      "3\n",
      "0\n",
      "%的违约金。\n",
      "3.乙方所交货物品种\n",
      "、规格、质量不符合合同规定的，如果甲方同意利用，应当按质论价；如果甲方不同意或不能利用的，由乙方负责按甲方要求包换、包修、包退，并承担修理、调换或退货而支付的实际费用。乙方不能修理或者不能调换的，按不能交货处理。\n",
      "4.乙方未按合同规定的货物数量交货时，少交的部分，甲方如果需要，应照数补交。甲方如不需要，可以退货。由于退货所造成的损失，由乙方承担。\n",
      "5.货物包装不符合合同规定时，乙方必须返修或重新包装，并承担返修或重新包装的费用。甲方不要求返修或重新包装而要求赔偿损失的，乙方应当偿付甲方该不合格包装物低于合格包装物的价值部分。因包装不符合规定造成货物损坏或灭失的，乙方应当负责赔偿。\n",
      "八\n",
      "、甲方责任\n",
      "1.甲方无正当理由违反合同规定拒绝接货的，应当承担由此造成的损失。\n",
      "2.甲方应按规定日期向乙方付款。\n",
      "如果逾期付款，应就逾期付款部分按照中国人民银行规定的同期贷款利率向乙方支付违约金。\n",
      "九、合同争议的解决方式\n",
      "本合同在履行过程中发生的争议，由双方当事人协商解决；协商或调解不成的依法向合同签订地人民法院起诉。\n",
      "十、送达\n",
      "1.\n",
      "本合同项下任何一方向对方发出的通知、信件、数据电文等，应当发送至本合同下列约定的地址、联系人和通信终端。一方当事人变更名称、地址、联系人或通信终端的，应当在变更后3日内及时书面通知对方当事人，对方当事人实际收到变更通知前的送达仍为有效送达，电子送达与书面送达具有同等法律效力。\n",
      "2\n",
      ".\n",
      "本条第一款约定的地址、联系人及电子通信终端亦为双方工作联系往来、法律文书及争议解决时人民法院的法律文书送达地址，人民法院的诉讼文书（含裁判文书）向合同任何一方当事人的上述地址送达的，视为有效送达。当事人对电子通信终端的联系送达适用于争议解决时的送达。 \n",
      "3\n",
      ".\n",
      "合同送达条款与保密条款、争议解决条款均为独立条款，不受合同整体或其他条款的效力的影响。\n",
      "十一、其他\n",
      "本合同自双方法定代表人或委托代理人签字盖章后生效，至合同条款履行完毕时终止。\n",
      "合同如有未尽事宜，须经双方共同协商，签订补充协议，补充协议与本合同具\n",
      "有同等效力。本合同一式\n",
      "二\n",
      "份，甲方执\n",
      "一\n",
      "份，乙方执\n",
      "一\n",
      "份。\n",
      "（以下无正文。）\n"
     ]
    }
   ],
   "source": [
    "import docx #导入docx库\n",
    "doc = Document(\"data/合同模板.docx\") #打开word文件\n",
    "for para in doc.paragraphs: #读取word中的每个段落\n",
    "    for run in para.runs: #读取每个段落中的不同格式（run）\n",
    "        print(run.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过以上程序，我们打印显示了合同里面的所有的格式（其中每一行代表一个格式（run））对应的文本（text），我们可以看到“【....】”都是在一行里面的，这样就没问题。由于word版合同里还有一些是在表格里面的，通过`doc.paragraphs`是无法抓取出来的，此时需要用`doc.tables`，表格（tables）里面又包含行(rows)，行还包含单元格(cell)，所以需要读取所有的表格，然后读取所有的行，再读取单元格，并打印显示出来。可见 “【....】” 也是在一行里面的，这样可保证后续替换时可查找到，不会导致遗漏。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "甲方（盖章）：【采购方】 \n",
      "\n",
      "法人代表：\n",
      "或委托代理人：\n",
      "\n",
      "开户行： 【开户行】\n",
      "账  号：【账号】\n",
      "联系人：【联系人】\n",
      "电  话：【电话】\n",
      "住  所：【住所】\n",
      "\n",
      "乙方（盖章）：ABC商贸有限公司\n",
      "\n",
      "法人代表：\n",
      "或委托代理人：\n",
      "\n",
      "开户行：中国建设银行\n",
      "账  号：989898989898\n",
      "联系人：张三丰\n",
      "电  话：999-99999\n",
      "住  所： 桃花源\n"
     ]
    }
   ],
   "source": [
    "for table in doc.tables:\n",
    "        for row in table.rows:\n",
    "            for cell in row.cells:\n",
    "                print(cell.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Word模板做好后，“【....】”内的信息就不可随意变动了，即便我们将“【合同编号】”里面的“遍”字删掉重新输入，结果还是“【合同编号】”，但此时“【合同编号】”已经不是一个格式了，会变成2个格式。如下示例显示了这个结果,“【合同编号】”已经不在同一行了。所以这个格式非常小气，不可轻易得罪啊！此时需要重新去Excel标题栏复制【合同编号】，再粘贴过去，保存，即可恢复同一格式（也可以在word中复制“【合同编号】”，覆盖粘贴成文本）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "合同编号：\n",
      "【合同编\n",
      "号】\n",
      " \n",
      "【货物名称】\n",
      "采购\n",
      "合同\n",
      "甲方\n",
      "：\n",
      "【采购方】\n",
      " \n"
     ]
    }
   ],
   "source": [
    "doc = Document(\"data\\合同模板 - 需填入部分格式错误.docx\") #打开word文件\n",
    "for para in doc.paragraphs: #读取word中的每个段落\n",
    "    for run in para.runs: #读取每个段落中的不同格式（run）\n",
    "        print(run.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "此实例虽然是采购合同，其处理方法适用于所有合同的批量生成，只需要准备好合同的模板，和需要填入合同的信息，剩下的就放心地交给Python吧。合同信息和模板准备好之后，就可开始批量替换，生成合同了。现在跟我出发。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "metadata": {},
   "outputs": [],
   "source": [
    "import docx\n",
    "def info_update(doc,old_info, new_info):\n",
    "    '''此函数用于批量替换合同中需要替换的信息\n",
    "    doc:合同模板\n",
    "    old_info和new_info：原文字和需要替换的新文字\n",
    "    '''\n",
    "    #读取段落中的所有run，找到需替换的信息进行替换\n",
    "    for para in doc.paragraphs: #\n",
    "        for run in para.runs:\n",
    "            run.text = run.text.replace(old_info, new_info) #替换信息\n",
    "    #读取表格中的所有单元格，找到需替换的信息进行替换\n",
    "    for table in doc.tables:\n",
    "        for row in table.rows:\n",
    "            for cell in row.cells:\n",
    "                cell.text = cell.text.replace(old_info, new_info) #替换信息"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为方便后续重复调用，以上我们定义了一个函数`info_update()`，它包含三个参数`doc,old_info, new_info`，分别代表word模板，原文本，和新文本。逐个读取word模板中的所有信息，只要遇到原文本，就替换成新文本。然后再读取word中的表格中的信息，也是遇到原文本，就替换成新文本。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "公司001合同完成\n",
      "公司002合同完成\n",
      "公司003合同完成\n",
      "公司004合同完成\n",
      "公司005合同完成\n",
      "公司006合同完成\n",
      "公司007合同完成\n",
      "公司008合同完成\n",
      "公司009合同完成\n",
      "公司010合同完成\n"
     ]
    }
   ],
   "source": [
    "from openpyxl import load_workbook #用于读取Excel中的信息\n",
    "wb = load_workbook('data/合同信息.xlsx')\n",
    "ws = wb.active\n",
    "doc = docx.Document(\"data/合同模板.docx\")\n",
    "for row in range(2, ws.max_row+1):\n",
    "    for col in range(1, ws.max_column+1):\n",
    "        #调用上面建立的函数，替换信息\n",
    "        info_update(doc,str(ws.cell(row=1,column=col).value), str(ws.cell(row=row,column=col).value))\n",
    "    doc.save(\"data/{}合同.docx\".format(str(ws.cell(row=row,column=3).value)))\n",
    "    print(\"{}合同完成\".format(str(ws.cell(row=row,column=3).value)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后使用“openpyxl”库的“load_workbook”模块，读物Excel档的合同信息，遍历每一行，每一列，调用替换信息的函数“info_update”完成合同信息替换，随后保存。\n",
    "\n",
    "我们以第一份合同为例，逐个看这些步骤是如何完成的。因为Excel中第一行是标题，合同信息是从第二行开始的，所以我们行是从2开始`row in range(2, ws.max_row+1)`，最大行加1结束（因为range函数是取不到最后一个数的，此例中最大行是11，如果不加1，则只能取到10，这样最后一份合同就会被漏掉了）。列也类似，不过是从第一列开始的`col in range(1, ws.max_column+1)`。\n",
    "\n",
    "第一份合同对应的row值为2，col值为1。原信息是Excel中的标题，对应也就是word中的“【....】”部分。次数原信息先取`ws.cell(row=1,column=1).value`，即如下所示，为'【合同编号】'。因为Excel表中有一些数字，加上str()是为了转换为字符串。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'【合同编号】'"
      ]
     },
     "execution_count": 78,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ws.cell(row=1,column=1).value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "新信息为`ws.cell(row=2,column=2).value`，如下所示。然后就将word中'【合同编号】'替换为'手机'，再替换第二列，第三列.....直到替换完所有的列，于是第一份合同生成完成，我们使用`doc.save`保存。我们给保存的文件名加上公司名称，以便于区分，公司名是Excel中第三列的值`ws.cell(row=row,column=3).value`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'手机'"
      ]
     },
     "execution_count": 79,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ws.cell(row=2,column=2).value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "第一份合同完成后，回到for循环，开始第二份合同的替换和保存，直到搞定所有合同。最终成果如下：\n",
    "![](images\\result.png)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
